const Buffer = require('buffer').Buffer
const crypto = require('crypto')

const parser = require('./parser')
const util = require('./util')

module.exports = {
  connectReq () {
    // Offset  Size            Name            Value
    // 0       64-bit integer  connection_id   0x41727101980
    // 8       32-bit integer  action          0 // connect
    // 12      32-bit integer  transaction_id  ? // random
    // 16

    const buf = Buffer.alloc(16)

    // connection_id
    buf.writeUInt32BE(0x417, 0)
    buf.writeUInt32BE(0x27101980, 4)
    // action
    buf.writeUInt32BE(0, 8)
    // transaction id
    crypto.randomBytes(4).copy(buf, 12)

    return buf
  },
  connectRes (res) {
    // Offset  Size            Name            Value
    // 0       32-bit integer  action          0 // connect
    // 4       32-bit integer  transaction_id
    // 8       64-bit integer  connection_id
    // 16

    return {
      action: res.readUInt32BE(0),
      transactionId: res.readUInt32BE(4),
      connectionId: res.slice(8)
    }
  },
  trackerReq (connectionId, torrent, port = 6881) {
    // Offset  Size    Name    Value
    // 0       64-bit integer  connection_id
    // 8       32-bit integer  action          1 // announce
    // 12      32-bit integer  transaction_id
    // 16      20-byte string  info_hash
    // 36      20-byte string  peer_id
    // 56      64-bit integer  downloaded
    // 64      64-bit integer  left
    // 72      64-bit integer  uploaded
    // 80      32-bit integer  event           0 // 0: none; 1: completed; 2: started; 3: stopped
    // 84      32-bit integer  IP address      0 // default
    // 88      32-bit integer  key             ? // random
    // 92      32-bit integer  num_want        -1 // default
    // 96      16-bit integer  port            ? // should be betwee
    // 98

    const buf = Buffer.allocUnsafe(98)

    // connection id
    connectionId.copy(buf, 0)
    // action
    buf.writeUInt32BE(1, 8)
    // transaction id
    crypto.randomBytes(4).copy(buf, 12)
    // info hash
    parser.infoHash(torrent).copy(buf, 16)
    // peerId
    util.genId().copy(buf, 36)
    // downloaded
    Buffer.alloc(8).copy(buf, 56)
    // left
    parser.size(torrent).copy(buf, 64)
    // uploaded
    Buffer.alloc(8).copy(buf, 72)
    // event
    buf.writeUInt32BE(0, 80)
    // ip address
    buf.writeUInt32BE(0, 84)
    // key
    crypto.randomBytes(4).copy(buf, 88)
    // num want
    buf.writeInt32BE(-1, 92)
    // port
    buf.writeUInt16BE(port, 96)

    return buf
  },
  trackerRes (res) {
    // Offset      Size            Name            Value
    // 0           32-bit integer  action          1 // announce
    // 4           32-bit integer  transaction_id
    // 8           32-bit integer  interval
    // 12          32-bit integer  leechers
    // 16          32-bit integer  seeders
    // 20 + 6 * n  32-bit integer  IP address
    // 24 + 6 * n  16-bit integer  TCP port
    // 20 + 6 * N

    if (res.length <= 20) {
      return {
        peers: []
      }
    }

    // extract peers
    const rawAddress = res.slice(20)
    const addressLen = 6
    let addressList = []
    for (let i = 0; i < rawAddress.length; i += addressLen) {
      addressList.push(rawAddress.slice(i, i + addressLen))
    }
    const peers = addressList.map(address => {
      return `${address.slice(0, 4).join('.')}:${address.readUInt16BE(4)}`
    })

    return {
      action: res.readUInt32BE(0),
      transactionId: res.readUInt32BE(4),
      leechers: res.readUInt32BE(8),
      seeders: res.readUInt32BE(12),
      peers
    }
  },

  // TCP message below
  handsShake (torrent) {
    // handshake: <pstrlen><pstr><reserved><info_hash><peer_id>
    // ----------
    // pstrlen: string length of <pstr>, as a single raw byte
    // pstr: string identifier of the protocol
    // reserved: eight (8) reserved bytes. All current implementations use all zeroes. Each bit in these bytes can be used to change the behavior of the protocol. An email from Bram suggests that trailing bits should be used first, so that leading bits may be used to change the meaning of trailing bits.
    // info_hash: 20-byte SHA1 hash of the info key in the metainfo file. This is the same info_hash that is transmitted in tracker requests.
    // peer_id: 20-byte string used as a unique ID for the client. This is usually the same peer_id that is transmitted in tracker requests (but not always e.g. an anonymity option in Azureus).
    // ----------
    // In version 1.0 of the BitTorrent protocol, pstrlen = 19, and pstr = "BitTorrent protocol".

    const buf = Buffer.alloc(68)

    // pstrlen
    buf.writeUInt8(19, 0)
    // pstr
    buf.write('BitTorrent protocol', 1)
    // reserved
    buf.writeUInt32BE(0, 20)
    buf.writeUInt32BE(0, 24)
    // info hash
    parser.infoHash(torrent).copy(buf, 28)
    // peer id
    util.genId().copy(buf, 48)

    return buf
  },
  keepAlive () {
    // keep-alive: <len=0000>

    return Buffer.alloc(4)
  },
  choke () {
    // choke: <len=0001><id=0>

    const buf = Buffer.alloc(5)

    // length
    buf.writeUInt32BE(1, 0)
    // id
    buf.writeUInt8(0, 4)

    return buf
  },
  unchoke () {
    // unchoke: <len=0001><id=1>

    const buf = Buffer.alloc(5)

    // length
    buf.writeUInt32BE(1, 0)
    // id
    buf.writeUInt8(1, 4)

    return buf
  },
  interested () {
    // interested: <len=0001><id=2>

    const buf = Buffer.alloc(5)

    // length
    buf.writeUInt32BE(1, 0)
    // id
    buf.writeUInt8(2, 4)

    return buf
  },
  notInterested () {
    // not interested: <len=0001><id=3>

    const buf = Buffer.alloc(5)

    // length
    buf.writeUInt32BE(1, 0)
    // id
    buf.writeUInt8(3, 4)

    return buf
  },
  have (pieceIndex) {
    // have: <len=0005><id=4><piece index>

    const buf = Buffer.alloc(9)

    // length
    buf.writeUInt32BE(5, 0)
    // id
    buf.writeUInt8(4, 4)
    // piece index
    buf.writeUInt32BE(pieceIndex, 5)

    return buf
  },
  bitfield (bitfield) {
    // bitfield: <len=0001+X><id=5><bitfield>

    const buf = Buffer.alloc(bitfield.length + 1 + 4)

    // length
    buf.writeUInt32BE(bitfield.length + 1, 0)
    // id
    buf.writeUInt8(5, 4)
    // bitfield
    bitfield.copy(buf, 5)

    return buf
  },
  request (payload) {
    // request: <len=0013><id=6><index><begin><length>

    const buf = Buffer.alloc(17)

    // length
    buf.writeUInt32BE(13, 0)
    // id
    buf.writeUInt8(6, 4)
    // piece index
    buf.writeUInt32BE(payload.index, 5)
    // begin
    buf.writeUInt32BE(payload.begin, 9)
    // length
    buf.writeUInt32BE(payload.length, 13)

    return buf
  },
  piece (payload) {
    // piece: <len=0009+X><id=7><index><begin><block>

    const buf = Buffer.alloc(payload.block.length + 13)

    // length
    buf.writeUInt32BE(payload.block.length + 9, 0)
    // id
    buf.writeUInt8(7, 4)
    // piece index
    buf.writeUInt32BE(payload.index, 5)
    // begin
    buf.writeUInt32BE(payload.begin, 9)
    // block
    payload.block.copy(buf, 13)

    return buf
  },
  cancle (payload) {
    // cancel: <len=0013><id=8><index><begin><length>

    const buf = Buffer.alloc(17)

    // length
    buf.writeUInt32BE(13, 0)
    // id
    buf.writeUInt8(8, 4)
    // piece index
    buf.writeUInt32BE(payload.index, 5)
    // begin
    buf.writeUInt32BE(payload.begin, 9)
    // length
    buf.writeUInt32BE(payload.length, 13)

    return buf
  },
  port (listenPort) {
    // port: <len=0003><id=9><listen-port>

    const buf = Buffer.alloc(7)

    // length
    buf.writeUInt32BE(3, 0)
    // id
    buf.writeUInt8(9, 4)
    // listen-port
    buf.writeUInt16BE(listenPort, 5)

    return buf
  },

  extractMsg (msg) {
    // Only keep-alive's length is not longer than 4.
    const id = msg.length > 4 ? msg.readInt8(4) : null

    // Message is longer than 5 (id >= 4) will have a payload.
    let payload = msg.length > 5 ? msg.slice(5) : null

    if (id === 6 || id === 7 || id === 8) {
      // request, piece and cancle have <index>.
      const rest = payload.slice(8)

      payload = {
        index: payload.readInt32BE(0),
        begin: payload.readInt32BE(4)
      }

      // request and cancel: <index><begin><length>
      // piece: <index><begin><block>
      payload[id === 7 ? 'block' : 'length'] = rest
    }

    return {
      size: msg.readInt32BE(0),
      id: id,
      payload
    }
  }
}